Skip to content

feat(app-core): n8n runtime-context provider — surface Discord guilds/channels + Gmail to workflow generator#7163

Merged
lalalune merged 2 commits into
elizaOS:developfrom
2-A-M:milady/n8n-runtime-context-provider
Apr 29, 2026
Merged

feat(app-core): n8n runtime-context provider — surface Discord guilds/channels + Gmail to workflow generator#7163
lalalune merged 2 commits into
elizaOS:developfrom
2-A-M:milady/n8n-runtime-context-provider

Conversation

@2-A-M
Copy link
Copy Markdown
Contributor

@2-A-M 2-A-M commented Apr 28, 2026

Summary

Registers an optional service of type n8n_runtime_context_provider that the patched @elizaos/plugin-n8n-workflow (see #25) reads to inject live connector facts into the workflow-generation prompt:

  • Discord facts — enumerates the bot's joined guilds + their text channels via the Discord REST API, emits one fact line per guild (Discord guild \"Cozy Devs\" (id …) channels: #general (id …), #alerts (id …).). 5-minute REST cache keeps generate→modify regeneration bursts cheap.
  • Gmail fact — surfaces the connected Gmail address so the LLM substitutes the real value verbatim instead of ``.
  • Supported credentials — only advertises cred types that the host's optional credProvider.resolve() confirms have data right now, so we don't promise a credential the user hasn't wired up yet. Without a registered credProvider, falls back to "config has connector token" heuristics.

Together with the prompt hardening in plugin-n8n-workflow#25, this closes the placeholder-id gap that previously made the LLM emit guildId: \"={{YOUR_SERVER_ID}}\" when the runtime already knew the real ID.

Why now

#7134's missing-credentials banner can tell the user a credential isn't wired yet. This PR closes the next loop: when credentials are wired, the generator gets the real values up front so it doesn't emit placeholders that need post-deploy fix-up.

Changes

  • New packages/app-core/src/services/n8n-runtime-context-provider.ts (~420 lines, self-contained — ConnectorConfigLike and the supported-cred-types set are inlined so this doesn't drag a sibling credential-provider port along).
  • New tests (n8n-runtime-context-provider.test.ts, 268 lines, 8 unit tests):
    • returns empty facts when no Discord token is configured;
    • emits one fact line per guild + caches subsequent calls within TTL;
    • supportedCredentials filtered against credProvider.resolve();
    • preferredProviders derived purely from connector config (no node search);
    • REST failures degrade to empty facts.
  • Wire-up in runtime/eliza.ts: ensureN8nRuntimeContextProvider follows the same hot-reload pattern as ensureN8nAuthBridge/ensureN8nAutoStart/ensureN8nDispatchService. Picks up the optional n8n_credential_provider if one is already registered.

Backward compat

100% additive. The plugin treats the service as optional — when not registered, the prompt simply omits the new ## Available Credentials and ## Runtime Facts sections (current behavior).

Depends on

  • Runtime depends on elizaos-plugins/plugin-n8n-workflow#25 (RuntimeContextProvider extension point). Host compiles fine without the plugin upgrade, but the prompt facts only take effect once Likes, retweets and quote tweets #25 merges and the plugin pointer bumps.

Test plan

  • bun run test packages/app-core/src/services/n8n-runtime-context-provider.test.ts — 8/8 pass.
  • With a configured Discord bot + Gmail, generate a Discord-routed workflow and inspect the prompt log: ## Runtime Facts block populated with guild + channel listing.
  • Without any connector configured: prompt unchanged from develop today (sections omitted).

Greptile Summary

This PR introduces a new n8n_runtime_context_provider service that injects live Discord guild/channel IDs and Gmail addresses into the n8n workflow-generation prompt, closing the placeholder-ID gap where the LLM would previously emit guildId: \"={{YOUR_SERVER_ID}}\". The implementation follows the existing hot-reload pattern in eliza.ts and the previously-flagged CRED_TYPE_FACTS/MILADY_SUPPORTED_CRED_TYPES mismatch has been corrected with a guard comment.

Confidence Score: 5/5

Safe to merge; all previous P1 issues have been addressed and remaining findings are P2 style/consistency concerns.

All P1 issues from previous review rounds are resolved. The two new findings are both P2: a silent guild omission on channel-fetch network errors (vs. the consistent behavior on HTTP errors), and a stale comment in eliza.ts. Neither affects correctness of the happy path.

packages/app-core/src/services/n8n-runtime-context-provider.ts — the per-guild catch block (lines 291–300) should push a fallback fact line for consistency.

Important Files Changed

Filename Overview
packages/app-core/src/services/n8n-runtime-context-provider.ts New service (~420 lines) that surfaces Discord guild/channel IDs and Gmail address to the n8n workflow generator; previous P1 issues (CRED_TYPE_FACTS mismatch, cache key) are addressed with code comments; one new P2: thrown per-guild channel fetches silently drop the guild from facts instead of pushing a fallback line.
packages/app-core/src/runtime/eliza.ts Wires up ensureN8nRuntimeContextProvider following the established hot-reload pattern; minor: inline comment about "config has connector token" fallback does not match the actual implementation.
packages/app-core/src/services/n8n-runtime-context-provider.test.ts 268-line test suite covering 8 scenarios including guild enumeration, caching, credProvider filtering, and graceful network failure degradation; coverage is adequate for the new service.

Sequence Diagram

sequenceDiagram
    participant Plugin as plugin-n8n-workflow
    participant Provider as N8nRuntimeContextProvider
    participant CredProv as n8n_credential_provider
    participant Discord as Discord REST API
    participant Cache as discordCache (in-process)

    Plugin->>Provider: getRuntimeContext({userId, relevantNodes, relevantCredTypes})
    Provider->>CredProv: resolve(userId, credType) [for each relevantCredType]
    CredProv-->>Provider: {status: credential_data} | {status: needs_auth}
    Note over Provider: Filter supportedCredentials to resolved types only

    alt Discord node in relevantNodes and token configured
        Provider->>Cache: get(botToken)
        alt Cache hit within 5 min TTL
            Cache-->>Provider: cached facts[]
        else Cache miss
            Provider->>Discord: GET /users/@me/guilds
            Discord-->>Provider: [{id, name}, ...]
            loop per guild
                Provider->>Discord: GET /guilds/{id}/channels
                Discord-->>Provider: [{id, name, type}, ...]
            end
            Provider->>Cache: set(botToken, {facts, expiresAt})
        end
    end

    alt Gmail node in relevantNodes and email configured
        Note over Provider: Push Connected Gmail account email
    end

    Provider-->>Plugin: {supportedCredentials[], facts[]}
Loading

Reviews (2): Last reviewed commit: "fix(n8n-runtime-context): drop discordWe..." | Re-trigger Greptile

…rd guilds/channels + Gmail email to the workflow generator

Registers a service of type `n8n_runtime_context_provider` so the patched
`@elizaos/plugin-n8n-workflow` (RuntimeContextProvider extension point)
can pull live connector facts into the workflow-generation prompt:

- **Discord facts**: enumerates the bot's joined guilds + their text
  channels via the Discord REST API, emitting one fact line per guild
  (`Discord guild "Cozy Devs" (id …) channels: #general (id …), #alerts
  (id …).`). 5-minute REST cache keeps generate→modify regeneration
  bursts cheap. Network failures degrade to empty facts; never block
  generation.
- **Gmail fact**: surfaces the connected Gmail address so the LLM
  substitutes the real value instead of `<your-email-here>`.
- **Supported credentials**: only advertises cred types that the host's
  optional `credProvider.resolve()` confirms have data right now (so we
  don't promise a credential the user hasn't wired up yet). Without a
  credProvider, falls back to "config has connector token" heuristics.

Together with the prompt hardening shipped in plugin-n8n-workflow#25,
this closes the placeholder-id gap that previously made the LLM emit
`guildId: "={{YOUR_SERVER_ID}}"` when the runtime already knew the real
ID.

Wire-up in `runtime/eliza.ts` follows the same hot-reload pattern as
the other n8n bridges. The provider is optional from the plugin's
perspective: when not registered, the prompt simply omits the
`## Available Credentials` and `## Runtime Facts` sections.

Includes 8 unit tests (`n8n-runtime-context-provider.test.ts`).

Depends on: elizaos-plugins/plugin-n8n-workflow#25 at runtime — host
compiles fine without the plugin upgrade, but the prompt hardening only
takes effect once the plugin's RuntimeContextProvider extension point
ships.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59a38bbe-cda9-49a8-8c76-d2ddbedf20a6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +65 to +78
const MILADY_SUPPORTED_CRED_TYPES: ReadonlySet<string> = new Set([
"discordApi",
"discordBotApi",
"discordWebhookApi",
"telegramApi",
"gmailOAuth2",
"gmailOAuth2Api",
"googleOAuth2Api",
"googleSheetsOAuth2Api",
"googleCalendarOAuth2Api",
"googleDriveOAuth2Api",
"slackApi",
"slackOAuth2Api",
]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 MILADY_SUPPORTED_CRED_TYPES / CRED_TYPE_FACTS mismatch silently drops Discord webhooks and generic Google OAuth

MILADY_SUPPORTED_CRED_TYPES includes "discordWebhookApi" and "googleOAuth2Api", but neither key exists in CRED_TYPE_FACTS. In computeSupportedCredentials, the code checks MILADY_SUPPORTED_CRED_TYPES.has(credType) first and then does const meta = CRED_TYPE_FACTS[credType]; if (!meta) continue; — so both types pass the first guard but are silently skipped by the second. A Discord webhook workflow will never receive credential metadata in supportedCredentials, defeating the purpose of this service for that workflow type.

Comment on lines +229 to +233
const fetchDiscordFacts = async (botToken: string): Promise<string[]> => {
const cached = discordCache.get(botToken);
if (cached && cached.expiresAt > now()) {
return cached.facts;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security Raw bot token used as cache key

discordCache.get(botToken) stores the raw Discord bot token as a Map key. While this is in-process memory and not persisted, it means the secret credential lives indefinitely as a map key (beyond its use for the actual request), surviving heap snapshots or debug tooling that iterates the map. A stable, non-secret key (e.g. a short hash of the token, or just a constant "default" since there is only ever one Discord token per config) would avoid keeping the secret in a data structure that may appear in diagnostics.

Comment on lines +259 to +296
const facts: string[] = [];
for (const guild of guilds) {
try {
const channelsRes = await fetchImpl(
`https://discord.com/api/v10/guilds/${guild.id}/channels`,
{ headers },
);
if (!channelsRes.ok) {
facts.push(
`Discord guild "${guild.name}" (id ${guild.id}) — channels not enumerable (status ${channelsRes.status}).`,
);
continue;
}
const channels = (await channelsRes.json()) as Array<{
id: string;
name: string;
type: number;
}>;
// type === 0 is GUILD_TEXT, the only kind n8n's Discord node posts to.
const textChannels = channels
.filter((c) => c.type === 0)
.map((c) => `#${c.name} (${c.id})`)
.join(", ");
facts.push(
textChannels.length > 0
? `Discord guild "${guild.name}" (id ${guild.id}) channels: ${textChannels}.`
: `Discord guild "${guild.name}" (id ${guild.id}) — no text channels visible to the bot.`,
);
} catch (err) {
runtime.logger.warn?.(
{
src: "n8n-runtime-context-provider",
guildId: guild.id,
err: err instanceof Error ? err.message : String(err),
},
"Discord channels REST threw",
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Sequential per-guild channel fetches with no rate-limit handling

Channels are fetched guild-by-guild in a for loop, making O(n) sequential requests against the Discord REST API. Discord enforces per-route rate limits (5 requests/5 s on most guild-scoped endpoints). If the bot is a member of many guilds, a single uncached getRuntimeContext call can exhaust the rate limit for the channels endpoint and start receiving 429 responses. The !channelsRes.ok branch appends a "not enumerable" fact rather than surfacing the rate-limit condition, so the caller sees degraded data silently.

Comment on lines +406 to +415
return {
service,
stop: () => {
try {
runtime.services.delete(SERVICE_TYPE as never);
} catch {
// ignore — symmetric with other Milady bridge stop hooks
}
},
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Handle's stop() does not call service.stop()

The outer handle's stop() only removes the service from runtime.services. It never calls service.stop(), which is the only place discordCache.clear() is invoked. On hot-reload, ensureN8nRuntimeContextProvider calls _n8nRuntimeContextProvider.stop() (the handle's stop), so the old cache closure is orphaned rather than explicitly cleaned up. Since a new closure is created on each call this is GC-safe, but it is asymmetric with the service.stop contract.

…om supported set

Both types were listed in MILADY_SUPPORTED_CRED_TYPES but had no entry in
CRED_TYPE_FACTS, so they passed the first guard in
computeSupportedCredentials() and then immediately dropped at the
`!meta` continue. Net effect: silently empty supportedCredentials for
Discord webhook workflows and generic Google OAuth (Greptile P1).

Drop them from the supported set instead of inventing fact entries —
Discord workflows go through the bot API path (discordBotApi) and the
specific google*OAuth2Api types cover the actual nodes we surface.
Comment block now flags the constraint so future additions stay in
sync with CRED_TYPE_FACTS.
@lalalune lalalune merged commit 78310a2 into elizaOS:develop Apr 29, 2026
3 checks passed
lalalune added a commit that referenced this pull request May 3, 2026
feat(app-core): n8n runtime-context provider — surface Discord guilds/channels + Gmail to workflow generator
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants